package plsqlfs;

import java.io.FilePermission;
import java.security.AccessControlException;
import java.security.Permission;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;

import com.google.common.annotations.VisibleForTesting;

/**
 * A class written to expose Java's file system operations as PL/SQL stored procedures.
 * The unusual syntax is caused by this need to interoperate with PL/SQL. For example, all
 * methods return either {@link #SUCCESS} or {@link #FAILURE}. All methods have a {@code int[]} parameter
 * named {@code errorNumber} and a {@code String[]} parameter named {@code errorMessage}. If 
 * the method returns {@link #FAILURE}, {@code errorNumber[0]} will contain the error code and
 * {@code errorMessage[0]} will contain the error message.
 * 
 * @author Adam Paynter
 *
 */
public class PlSqlFileSystem {



	/**
	 * The {@code int} value used to represent {@code true}.
	 */
	public static final int TRUE = 1;

	/**
	 * The {@code int} value used to represent {@code false}.
	 */
	public static final int FALSE = 0;



	/**
	 * The {@code int} value returned from methods if their operation completed successfully.
	 */
	public static final int SUCCESS = 1;

	/**
	 * The {@code int} value returned from methods if their operation failed.
	 */
	public static final int FAILURE = 0;



	private static int toInt(boolean v) {
		return v ? TRUE : FALSE;
	}



	private final FileSystem fileSystem;
	private final Connection connection;
	private final Map<Integer, FileSystem.FileHandle> fileHandlesById = new HashMap<Integer, FileSystem.FileHandle>();
	private String varcharTableTypeName = "FS_VARCHAR_TABLE_TYPE";
	private String errorTypeName = "FS_ERROR_TYPE";
	private String fileHandleTypeName = "FS_FILE_HANDLE_TYPE";
	private ArrayDescriptor varcharTableTypeDescriptor;



	public PlSqlFileSystem(FileSystem fileSystem, Connection connection) {
		this.fileSystem = fileSystem;
		this.connection = connection;
	}

	public PlSqlFileSystem(FileSystem fileSystem) throws SQLException {
		this( fileSystem, DriverManager.getConnection("jdbc:default:connection:") );
	}

	@VisibleForTesting
	protected PlSqlFileSystem() throws SQLException {
		this( new FileSystem(), (Connection) null );
	}

	public String getVarcharTableTypeName() {
		return varcharTableTypeName;
	}

	private ArrayDescriptor getVarcharTableTypeDescriptor() throws SQLException {
		if (varcharTableTypeDescriptor == null) {
			varcharTableTypeDescriptor = ArrayDescriptor.createDescriptor(getVarcharTableTypeName(), getConnection());
		}
		return varcharTableTypeDescriptor;
	}

	public void setVarcharTableTypeName(String varcharTableTypeName) {
		this.varcharTableTypeName = varcharTableTypeName;
	}

	public String getErrorTypeName() {
		return errorTypeName;
	}

	public void setErrorTypeName(String errorTypeName) {
		this.errorTypeName = errorTypeName;
	}

	public String getFileHandleTypeName() {
		return fileHandleTypeName;
	}

	public void setFileHandleTypeName(String fileHandleTypeName) {
		this.fileHandleTypeName = fileHandleTypeName;
	}

	private FileSystem.FileHandle internalFileHandle(FileHandle fileHandle) throws SQLException {
		return fileHandle(fileHandle.getId());
	}

	private FileSystem.FileHandle fileHandle(int fileHandleId) {
		return fileHandlesById.get(fileHandleId);
	}

	private FileHandle registerFileHandle(FileSystem.FileHandle fileHandle) {
		int id = fileHandle.getFileHandleId();
		fileHandlesById.put(id, fileHandle);
		return new FileHandle(fileHandleTypeName, id);
	}

	private Connection getConnection() throws SQLException {
		return connection;
	}

	private ARRAY createVarcharTable(Collection<? extends String> values) throws SQLException {
		return createVarcharTable(values.toArray(new String[values.size()]));
	}

	private ARRAY createVarcharTable(String[] values) throws SQLException {
		return new ARRAY(getVarcharTableTypeDescriptor(), getConnection(), values);
	}



	protected int success() {
		return SUCCESS;
	}

	protected int failure(Exception cause, FileSystemException[] error) throws Exception {
		FileSystemException fileSystemException = toFileSystemException(cause);
		fileSystemException.setSQLTypeName(errorTypeName);
		error[0] = fileSystemException;
		return FAILURE;
	}

	private FileSystemException toFileSystemException(Exception cause) {
		if (cause instanceof FileSystemException) {
			return (FileSystemException) cause;
		}
		
		if (cause instanceof AccessControlException) {
			AccessControlException accessControlException = (AccessControlException) cause;
			
			Permission permission = accessControlException.getPermission();
			if (permission instanceof FilePermission) {
				FilePermission filePermission = (FilePermission) permission;
				
				StringBuilder plsql = new StringBuilder();
				plsql.append("BEGIN fs.grant_permissions(path => '");
				plsql.append(filePermission.getName());
				plsql.append("'");
				
				Set<String> actions = new HashSet<String>(Arrays.asList(filePermission.getActions().split(",")));
				
				if (actions.contains("read")) {
					plsql.append(", readable => TRUE");
				}
				
				if (actions.contains("write")) {
					plsql.append(", writable => TRUE");
				}
				
				if (actions.contains("execute")) {
					plsql.append(", executable => TRUE");
				}
				
				if (actions.contains("delete")) {
					plsql.append(", deletable => TRUE");
				}
				
				plsql.append("); END;");
				
				return new InsufficientPrivilegesException("Insufficient privileges. " + plsql, cause);
			} else {
				return new InsufficientPrivilegesException(cause.getMessage(), cause);
			}
		}
		
		return new FileSystemException(FileSystemException.UNKNOWN, cause.getMessage(), cause);
	}



	public int appendToPath(String basePath, String relativePath, String[] newPath, FileSystemException[] error) throws Exception {
		// TODO Auto-generated method stub
		try {
			newPath[0] = fileSystem.appendToPath(basePath, relativePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int getResolvedPath(String path, String[] resolvedPath, FileSystemException[] error) throws Exception {
		try {
			resolvedPath[0] = fileSystem.resolvedPath(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int absolutePath(String path, String[] absolutePath, FileSystemException[] error) throws Exception {
		try {
			absolutePath[0] = fileSystem.absolutePath(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int canonicalPath(String path, String[] canonicalPath, FileSystemException[] error) throws Exception {
		try {
			canonicalPath[0] = fileSystem.canonicalPath(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int getResolvedFilePath(String filePath, String[] resolvedFilePath, FileSystemException[] error) throws Exception {
		try {
			resolvedFilePath[0] = fileSystem.resolvedFilePath(filePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int absoluteFilePath(String filePath, String[] absoluteFilePath, FileSystemException[] error) throws Exception {
		try {
			absoluteFilePath[0] = fileSystem.absoluteFilePath(filePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int canonicalFilePath(String filePath, String[] canonicalFilePath, FileSystemException[] error) throws Exception {
		try {
			canonicalFilePath[0] = fileSystem.canonicalFilePath(filePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int getResolvedDirectoryPath(String directoryPath, String[] resolvedDirectoryPath, FileSystemException[] error) throws Exception {
		try {
			resolvedDirectoryPath[0] = fileSystem.resolvedDirectoryPath(directoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int absoluteDirectoryPath(String directoryPath, String[] absoluteDirectoryPath, FileSystemException[] error) throws Exception {
		try {
			absoluteDirectoryPath[0] = fileSystem.absoluteDirectoryPath(directoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int canonicalDirectoryPath(String directoryPath, String[] canonicalDirectoryPath, FileSystemException[] error) throws Exception {
		try {
			canonicalDirectoryPath[0] = fileSystem.canonicalDirectoryPath(directoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int name(String path, String[] name, FileSystemException[] error) throws Exception {
		try {
			name[0] = fileSystem.name(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int base(String name, String[] base, FileSystemException[] error) throws Exception {
		try {
			base[0] = fileSystem.base(name);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int extension(String name, String[] extension, FileSystemException[] error) throws Exception {
		try {
			extension[0] = fileSystem.extension(name);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int fileName(String filePath, String[] fileName, FileSystemException[] error) throws Exception {
		try {
			fileName[0] = fileSystem.fileName(filePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int directoryName(String directoryPath, String[] directoryName, FileSystemException[] error) throws Exception {
		try {
			directoryName[0] = fileSystem.directoryName(directoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int fileSize(String path, long[] fileSize, FileSystemException[] error) throws Exception {
		// TODO Auto-generated method stub
		try {
			fileSize[0] = fileSystem.fileSize(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int exists(String path, int[] exists, FileSystemException[] error) throws Exception {
		try {
			exists[0] = toInt(fileSystem.exists(path));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}

	public int fileExists(String path, int[] exists, FileSystemException[] error) throws Exception {
		try {
			exists[0] = toInt(fileSystem.fileExists(path));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int delete(String path, FileSystemException[] error) throws Exception {
		try {
			fileSystem.delete(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int isAbsolute(String path, int[] isAbsolute, FileSystemException[] error) throws Exception {
		try {
			isAbsolute[0] = toInt(fileSystem.isAbsolute(path));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int isDirectory(String path, int[] isDirectory, FileSystemException[] error) throws Exception {
		try {
			isDirectory[0] = toInt(fileSystem.isDirectory(path));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int isFile(String path, int[] isFile, FileSystemException[] error) throws Exception {
		try {
			isFile[0] = toInt(fileSystem.isFile(path));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int touch(String path, FileSystemException[] error) throws Exception {
		try {
			fileSystem.touch(path);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int touchFile(String filePath, FileSystemException[] error) throws Exception {
		try {
			fileSystem.touchFile(filePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int touchDirectory(String directoryPath, FileSystemException[] error) throws Exception {
		try {
			fileSystem.touchDirectory(directoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listNames(String directoryPath, ARRAY[] names, FileSystemException[] error) throws Exception {
		try {
			names[0] = createVarcharTable(fileSystem.listNames(directoryPath));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listNames(String directoryPath, String pattern, ARRAY[] names, FileSystemException[] error) throws Exception {
		try {
			names[0] = createVarcharTable(fileSystem.listNames(directoryPath, pattern));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listPaths(String directoryPath, ARRAY[] paths, FileSystemException[] error) throws Exception {
		try {
			paths[0] = createVarcharTable(fileSystem.listPaths(directoryPath));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listPaths(String directoryPath, String pattern, ARRAY[] paths, FileSystemException[] error) throws Exception {
		try {
			paths[0] = createVarcharTable(fileSystem.listPaths(directoryPath, pattern));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listFileNames(String directoryPath, ARRAY[] fileNames, FileSystemException[] error) throws Exception {
		try {
			fileNames[0] = createVarcharTable(fileSystem.listFileNames(directoryPath));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listFileNames(String directoryPath, String pattern, ARRAY[] fileNames, FileSystemException[] error) throws Exception {
		try {
			fileNames[0] = createVarcharTable(fileSystem.listFileNames(directoryPath, pattern));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listFilePaths(String directoryPath, ARRAY[] filePaths, FileSystemException[] error) throws Exception {
		try {
			filePaths[0] = createVarcharTable(fileSystem.listFilePaths(directoryPath));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listFilePaths(String directoryPath, String pattern, ARRAY[] filePaths, FileSystemException[] error) throws Exception {
		try {
			filePaths[0] = createVarcharTable(fileSystem.listFilePaths(directoryPath, pattern));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listDirectoryNames(String directoryPath, ARRAY[] fileNames, FileSystemException[] error) throws Exception {
		try {
			fileNames[0] = createVarcharTable(fileSystem.listDirectoryNames(directoryPath));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listDirectoryNames(String directoryPath, String pattern, ARRAY[] fileNames, FileSystemException[] error) throws Exception {
		try {
			fileNames[0] = createVarcharTable(fileSystem.listDirectoryNames(directoryPath, pattern));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listDirectoryPaths(String directoryPath, ARRAY[] filePaths, FileSystemException[] error) throws Exception {
		try {
			filePaths[0] = createVarcharTable(fileSystem.listDirectoryPaths(directoryPath));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listDirectoryPaths(String directoryPath, String pattern, ARRAY[] filePaths, FileSystemException[] error) throws Exception {
		try {
			filePaths[0] = createVarcharTable(fileSystem.listDirectoryPaths(directoryPath, pattern));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int listRootDirectoryPaths(ARRAY[] rootDirectoryPaths, FileSystemException[] error) throws Exception {
		try {
			rootDirectoryPaths[0] = createVarcharTable(fileSystem.listRootDirectoryPaths());
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int copyFileToFile(String sourceFilePath, String destinationFilePath, FileSystemException[] error) throws Exception {
		try {
			fileSystem.copyFileToFile(sourceFilePath, destinationFilePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int copyFileToDirectory(String sourceFilePath, String destinationDirectoryPath, String[] destinationFilePath, FileSystemException[] error) throws Exception {
		try {
			destinationFilePath[0] = fileSystem.copyFileToDirectory(sourceFilePath, destinationDirectoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int moveFileToFile(String sourceFilePath, String destinationFilePath, FileSystemException[] error) throws Exception {
		try {
			fileSystem.moveFileToFile(sourceFilePath, destinationFilePath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int moveFileToDirectory(String sourceFilePath, String destinationDirectoryPath, String[] destinationFilePath, FileSystemException[] error) throws Exception {
		try {
			destinationFilePath[0] = fileSystem.moveFileToDirectory(sourceFilePath, destinationDirectoryPath);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int openBinaryFileForReading(String filePath, int bufferSize, FileHandle[] fileHandle, FileSystemException[] error) throws Exception {
		try {
			fileHandle[0] = registerFileHandle(fileSystem.openBinaryFileForReading(filePath, bufferSize));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int openBinaryFileForWriting(String filePath, int bufferSize, FileHandle[] fileHandle, FileSystemException[] error) throws Exception {
		try {
			fileHandle[0] = registerFileHandle(fileSystem.openBinaryFileForWriting(filePath, bufferSize));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int openTextualFileForReading(String filePath, String charsetName, int bufferSize, FileHandle[] fileHandle, FileSystemException[] error) throws Exception {
		try {
			fileHandle[0] = registerFileHandle(fileSystem.openTextualFileForReading(filePath, charsetName, bufferSize));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int openTextualFileForWriting(String filePath, String charsetName, int bufferSize, FileHandle[] fileHandle, FileSystemException[] error) throws Exception {
		try {
			fileHandle[0] = registerFileHandle(fileSystem.openTextualFileForWriting(filePath, charsetName, bufferSize));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int closeFileHandle(FileHandle fileHandle, FileSystemException[] error) throws Exception {
		try {
			fileSystem.closeFileHandle(internalFileHandle(fileHandle));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int hasMoreLines(FileHandle fileHandle, int[] result, FileSystemException[] error) throws Exception {
		try {
			result[0] = toInt(internalFileHandle(fileHandle).hasMoreLines());
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int readLine(FileHandle fileHandle, String[] line, FileSystemException[] error) throws Exception {
		try {
			line[0] = internalFileHandle(fileHandle).readLine();
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int readLines(String filePath, String charsetName, ARRAY[] lines, FileSystemException[] error) throws Exception {
		try {
			lines[0] = createVarcharTable(fileSystem.readLines(filePath, charsetName));
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int write(FileHandle fileHandle, String text, FileSystemException[] error) throws Exception {
		try {
			internalFileHandle(fileHandle).write(text);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



	public int writeLine(FileHandle fileHandle, String text, FileSystemException[] error) throws Exception {
		try {
			internalFileHandle(fileHandle).writeLine(text);
			
			return success();
		} catch (Exception e) {
			return failure(e, error);
		}
	}



}
